package com.videomaru.media
{
	import com.videomaru.IPlayable;
	import com.videomaru.Log;
	import com.videomaru.MaruEvent;
	import com.videomaru.MaruScaleMode;
	
	import flash.display.MovieClip;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.NetStatusEvent;
	import flash.events.SecurityErrorEvent;
	import flash.events.TimerEvent;
	import flash.media.SoundTransform;
	import flash.media.Video;
	import flash.net.NetConnection;
	import flash.net.NetStream;
	import flash.utils.*;
	
	/**
	 * com.videomaru.media.PlayFLV
	 * <p>The first media class (capability) for the AS3 version of video.Maru handles basic playback of FLV files.</p>
	 * <p>It implements the IPlayable interface, as must all future extensions of the media capabilities. The IPlayable interface defines all the basic functions of any media such as play, pause, stop and so on.</p>
	 * 
	 * @see com.videomaru.IPlayable          IPlayable
	 * @see com.videomaru.MaruPlayer         MaruPlayer
	 * 
	 * @author jensa {at} flashmagazine.com
	 * @version 0.3
	 */

	public class PlayFLV extends Sprite implements IPlayable
	{
        private var connection:NetConnection;
        private var stream:NetStream;
        private var vidstream:NetStream;
        
        private var _width:Number;
        private var _height:Number;
        private var _visible:Number;
        private var _realwidth:Number;
        private var _realheight:Number;
        private var _aspect:Number;
        
        private var _x:Number;
        private var _y:Number;
        private var video:Video;
        
        private var _mediatype:String;
        private var _autoplay:Boolean;
        private var _scaleMode:String;
        private var _isPlaying:Boolean;
        private var _duration:Number;
        private var _autorewind:Boolean;
        private var _fileURL:String;
        private var _smoothing:Boolean;
        
        private var _bufferTime:Number = 0.1;
        private var _bufferProgress:uint;
        private var _bufferProgressLastValue:uint;
        private var _downloadProgress:uint;
        private var _downloadProgressLastValue:uint;
        private var _playProgress:uint;
        private var _playProgressLastValue:uint;
        
        /*************************************************************
        * 
        * EXCEPTIONS
        * that may require some extra explanation
        * 
        *************************************************************/
        
        /*************************************************************
        * _realstart
        * @see progressUpdate
        *************************************************************/
        private var _realstart:Boolean;
		
		function PlayFLV(file:String,aPlay:Boolean = true,sMode:String = MaruScaleMode.NO_SCALE, aRewind:Boolean = false)
		{
			Log.dbg("new PlayFLV created "+arguments,Log.FUNCTION);
			mediatype = "flv";
			if(file.length > 0){
				fileURL = file;
				autoplay = aPlay;
				scaleMode = sMode;
				autorewind = aRewind;
				_isPlaying = false;
				_realstart = false;
				connection = new NetConnection();
	            connection.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler);
	            connection.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
	            connection.connect(null);
	            this.addEventListener(Event.ENTER_FRAME,progressUpdate);
	            this.addEventListener(Event.REMOVED_FROM_STAGE,remove);
			}
		}
		public function play():void
		{
			Log.dbg("PlayFLV.play "+fileURL+" _isPlaying: "+_isPlaying+" this.position "+this.position,Log.FUNCTION);
			if(this.position > 0){
				vidstream.resume();
				this.dispatchEvent( new MaruEvent( MaruEvent.START ) );
			} else {
				_realstart = false;
				vidstream.play(fileURL);
			}
		}
		
		public function pause():void
		{
			Log.dbg("PlayFLV.pause "+arguments,Log.FUNCTION);
			vidstream.pause();
			_isPlaying = false;
			this.dispatchEvent( new MaruEvent( MaruEvent.PAUSE ) );
		}
		
		public function stop():void // Note: this is not the same as the Netstream responding to a "NetStream.Play.Stop" event. 
		{
			Log.dbg("PlayFLV.stop "+arguments,Log.FUNCTION);
			vidstream.seek(0);
			_isPlaying = false;
			this.dispatchEvent( new MaruEvent( MaruEvent.STOP ) );
			// We want the video to reflect the new position after stopping
			// Since we can't force an update without an event that supports it, we'll start a minimal timer for that purpose
			var justWaitALittle:Timer = new Timer(20,1);
			justWaitALittle.addEventListener(TimerEvent.TIMER,onStop);
			justWaitALittle.start();
		}
		
		private function onStop(e:TimerEvent):void
		{
			e.updateAfterEvent();
			vidstream.close();
		}
		
		public function getDuration():Number
		{
			Log.dbg("PlayFLV.getDuration "+arguments);
			return duration;
		}
		public function getVol():Number
		{
			Log.dbg("PlayFLV.getVol "+arguments);
			var transform:SoundTransform = vidstream.soundTransform;
            var vol:Number = transform.volume;
			return vol;
		}
		public function setVol(v:Number):void
		{
			Log.dbg("PlayFLV.setVol "+arguments);
			var transform:SoundTransform = vidstream.soundTransform;
            transform.volume = v;
            vidstream.soundTransform = transform;
		}
		
		public function remove():void
		{
			Log.dbg("PlayFLV.remove "+fileURL,Log.FUNCTION);
			if(vidstream){
				vidstream.close();
				vidstream.removeEventListener(NetStatusEvent.NET_STATUS, netStatusHandler);
			}
			if(connection){
				connection.removeEventListener(NetStatusEvent.NET_STATUS, netStatusHandler);
				connection.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
			}
	        this.removeEventListener(Event.ENTER_FRAME,progressUpdate);
		}
		public function setSize(w:Number, h:Number):void
		{
			Log.dbg("PlayFLV.setSize "+arguments);
			if(w && h){
				_width = w;
				_height = h;
				redraw();
			}
		}
		public function set bufferTime(b:Number):void
		{
			_bufferTime = b;
		}
		public function get bufferTime():Number
		{
			return _bufferTime;
		}
		public function set position(t:Number):void
		{
			Log.dbg("PlayFLV.set position "+arguments+" _isPlaying: "+_isPlaying);
			if(t<0){
				t = 0;
			}
			vidstream.seek(t);
		}
		public function get position():Number
		{
			Log.dbg("PlayFLV.get position "+arguments);
			return vidstream.time;
		}
		public function set smoothing(b:Boolean):void
		{
			Log.dbg("PlayFLV.set smoothing "+arguments);
			_smoothing = b;
			video.smoothing = _smoothing;
		}
		public function get smoothing():Boolean
		{
			Log.dbg("PlayFLV.get smoothing "+arguments);
			return _smoothing;
		}
        
        /**
		* A string indicating the kind of media the class is capable of playing.  
		*/
		public function set mediatype(b:String):void
		{
			_mediatype = b;
		}
		public function get mediatype():String
		{
			return _mediatype;
		}
		/**
		* Will the media start playing by itself?
		*/
		public function set autoplay(b:Boolean):void
		{
			_autoplay = b;
		}
		public function get autoplay():Boolean
		{
			return _autoplay;
		}
		/**
		* Tells the player how to scale the media
		* @see com.videomaru.PlayerScaleMode   
		*/
        public function set scaleMode(b:String):void
        {
			_scaleMode = b;
		}
		public function get scaleMode():String
		{
			return _scaleMode;
		}
        /**
		* Holds the current play state
		*/
		public function get isPlaying():Boolean
		{
			return _isPlaying;
		}
        /**
		* Holds the duration of the media file
		*/
        public function set duration(b:Number):void
        {
			_duration = b;
		}
		public function get duration():Number
		{
			return _duration;
		}
        /**
		* When playback ends, should the media rewind to the start?
		*/
        public function set autorewind(b:Boolean):void
        {
			_autorewind = b;
		}
		public function get autorewind():Boolean
		{
			return _autorewind;
		}
        /**
		* The URL of the media to display
		*/
        public function set fileURL(b:String):void
        {
			_fileURL = b;
		}
		public function get fileURL():String
		{
			return _fileURL;
		}
		
	// Private functions
		private function redraw():void
		{
			Log.dbg("PlayFLV.redraw "+_aspect+" mode: "+scaleMode);
			if(scaleMode == MaruScaleMode.NO_SCALE){
				if(_realwidth && _realheight){
					video.width = _realwidth;
					video.height = _realheight;
				} else {
					video.width = _width;
					video.height = _height;
				}
			} else if(scaleMode == MaruScaleMode.EXACT_FIT){
				video.width = _width;
				video.height = _height;
			} else if(scaleMode == MaruScaleMode.MAINTAIN_ASPECT_RATIO && _aspect){
				video.width = _width;
				video.height = _width/_aspect;
			}
		}
		/**
		 * This event is called on ENTER_FRAME since we'll only need to update these values
		 * when the display is updated. 
		 * @private
		 */ 
		private function progressUpdate(e:Event):void{
			// Buffer
			_bufferProgress   = returnSafePercent( (vidstream.bufferLength * 100) / vidstream.bufferTime);
			if(_bufferProgressLastValue != _bufferProgress){
				_bufferProgressLastValue = _bufferProgress;
				this.dispatchEvent( new MaruEvent( MaruEvent.BUFFER_PROGRESS,false,false,"bufferProgress",_bufferProgress ) );
			}
			// Download
        	_downloadProgress = returnSafePercent( (vidstream.bytesLoaded * 100) / vidstream.bytesTotal);
        	if(_downloadProgressLastValue != _downloadProgress){
        		_downloadProgressLastValue = _downloadProgress;
				this.dispatchEvent( new MaruEvent( MaruEvent.DOWNLOAD_PROGRESS,false,false,"downloadProgress",_downloadProgress ) );
			}
        	// Playback
        	_playProgress     = returnSafePercent( (vidstream.time*100) / duration);
        	if(_playProgressLastValue != _playProgress){
        		_playProgressLastValue = _playProgress;
				this.dispatchEvent( new MaruEvent( MaruEvent.PLAY_PROGRESS,false,false,"playProgress",_playProgress ) );
				
				/***************************************************************
		        * Explanation: _realstart
		        * The native NetStream.Play.Start event does not trigger when
		        * it should. The expected behavior would be to dispatch an event
		        * when playback actually has started. We get around this bug by 
		        * waiting for the first progress-update. After this initial
		        * start, the event will trigger as it should.
		        * 
		        * To see this bug in actio, you'll need a tool such as Charles
		        * (http://xk72.com/charles/) to strangle the bandwidth since
		        * this bug is only perceivable on a slow connection 
		        ***************************************************************/
				if(!_realstart){
					_realstart = true;
					Log.dbg("_realstart occurred.");
					this.dispatchEvent( new MaruEvent( MaruEvent.START ) );
					redraw();
				}
			}
		}
		/**
		 * At any given time, one or more values in "progressUpdate" may be undefined.
		 * To prevent faulty numbers, we make sure all numbers are between 0 and 100.
		 * @private
		 */ 
		private function returnSafePercent(n:Number):Number{
			if(n > 0 && n < 100){
				// all fine
			}else if(n >= 100){
				n = 100;
			} else{
				n = 0;
			}
			return n;
		}
		
		/**
		 * We should problably detect more of the events available from NetStatusEvent.info
		 * but this will do for now. The full doc can be found here:
		 * http://livedocs.adobe.com/flash/9.0/ActionScriptLangRefV3/flash/events/NetStatusEvent.html#info
		 * @private
		 */ 
        private function netStatusHandler(event:NetStatusEvent):void
        {
            switch (event.info.code) {
                case "NetConnection.Connect.Success":
                    connectStream();
                    if(autoplay){
                    	play();
                    } else {
                    	stop();
                    }
                    break;
                case "NetStream.Play.StreamNotFound":
                    Log.dbg("Stream not found: " + fileURL);
                    this.dispatchEvent( new MaruEvent( MaruEvent.IO_ERROR ) );
                    _isPlaying = false;
                    break;
                case "NetStream.Play.Start":
                	_isPlaying = true;
                	var intervalId:uint = setTimeout(getAspect,50);// videoWidth and videoHeight will not report until after waiting one frame
                	/**
                	 * Only report .START events if the _realstart has actually happened.
                	 * @see 
                	 */
                    if(_realstart){
                    	this.dispatchEvent( new MaruEvent( MaruEvent.START ) );
                    } else {
                    	redraw();
                    }
                    break;
                case "NetStream.Buffer.Full":
                    this.dispatchEvent( new MaruEvent( MaruEvent.BUFFER_FULL ) );
                    break;
                case "NetStream.Play.Stop": // Note: this is not the same as the user stopping the video ( stop()-function )
                	_isPlaying = false;
					if(autorewind){
						stop();
					} else {
						vidstream.close();
					}
                    this.dispatchEvent( new MaruEvent( MaruEvent.COMPLETE ) );
                    break;
                case "NetStream.Seek.Notify":
                    this.dispatchEvent( new MaruEvent( MaruEvent.SEEK ) );
                    break;
                default :
                    Log.dbg("Unlogged: " + event.info.code);
                    break;
            }
        }
        /**
        * We should probably do this using onMetaData, but for some reason ENTER_FRAME feels safer?
        * Reasoning: Flash will automatically provde the correct width/heigth, but the metadata may
        * be incorrect so why trust them?
        * @private
        */
        private function getAspect():void
        {
            _aspect = video.videoWidth / video.videoHeight;
            Log.dbg("getAspect: " + video.videoWidth+"/" + video.videoHeight+" = "+_aspect,Log.FUNCTION);
            if(_aspect && video.videoWidth != 0 && video.videoHeight != 0){
	            _realwidth = video.videoWidth;
	            _realheight = video.videoHeight;
	            redraw();
	            this.dispatchEvent( new MaruEvent( MaruEvent.ASPECT ) );
	        } else if (video.videoWidth != 0 || video.videoHeight != 0){
	        	// TODO: Sometimes, video.videoWidth and video.videoHeight will report zero. Trap this bug! Maybe issue a timer that tries some milliseconds later?
	        	redraw();
	        }
        }
        
		/**
        * This will occur if loading from a non-legal domain (sandbox violation)
        * @private
        */
        private function securityErrorHandler(event:SecurityErrorEvent):void
        {
            Log.dbg("securityErrorHandler: " + event,Log.FUNCTION);
            this.dispatchEvent( new MaruEvent( MaruEvent.SECURITY_ERROR ) );
            var errMsg:String = "A security error occured.\nThis usually means that you have tried to load an file from a website that does not allow you to use their files. This can often be fixed if the website publishes a special file with the name 'crossdomain.xml'. See http://livedocs.adobe.com/flash/9.0/main/00000347.html for more information."
            Log.dbg(errMsg,Log.ERROR);
        }
        
        /**
        * This is our best shot at capturing the duration of video. The info may be incorrect,
        * but it's the best we have. NOTE: This method must be decleared as public, or calling
        * class will not be able to call it
        * @private
        */
        public  function onMetaData(info:Object):void
        {
        	Log.dbg("PlayFLV.onMetaData "+info.duration,Log.FUNCTION);
	        // info.framerate is also available
	        if(info.duration != null){ duration = info.duration; }
	        // Be sure that the variable is set before dispatching the event...
	        this.dispatchEvent( new MaruEvent(MaruEvent.METADATA,false,false,"duration",duration ) );
	    }
        /**
        * Detecting stream end
        * @private
        */
        public  function onPlayStatus(info:Object):void
        {
        	Log.dbg("PlayFLV.onPlayStatus "+info,Log.FUNCTION);
	        /* for(var each in info){
	        	trace("info: "+info[each]+" - "+each);
	        } */
	    }
        /**
        * Grabbing cuepoints
        * @private
        */
        public  function onCuePoint(info:Object):void
        {
        	Log.dbg("PlayFLV.onCuePoint "+info,Log.FUNCTION);
	        /* for(var each in info){
	        	trace("info: "+info[each]+" - "+each);
	        } */
	    }
		
        private function connectStream():void
        {
        	Log.dbg("PlayFLV.connectStream ",Log.FUNCTION);
            vidstream = new NetStream(connection);
            vidstream.bufferTime = _bufferTime;
            vidstream.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler);
            vidstream.client = this; // Sends onMetaData events to the null-object
            video = new Video();
            video.attachNetStream(vidstream);
            addChild(video);
            // Set width and height initially (if we are not to scale)
            if(scaleMode == MaruScaleMode.NO_SCALE && _width && _height){
            	video.width  = _width;
	            video.height = _height;
            }
        }
        
        public override function addEventListener(type:String,listener:Function,useCapture:Boolean=false,priority:int=0,useWeakReference:Boolean=false):void
        {
        	super.addEventListener(type,listener,useCapture,priority,useWeakReference);
        }
		public override function removeEventListener(type:String,listener:Function,useCapture:Boolean=false):void
        {
        	super.removeEventListener(type,listener,useCapture);
        }
		
	}
}